package org.dicr.sys.linux;

import java.io.*;
import java.lang.ref.*;
import java.util.*;
import java.util.logging.*;

import org.dicr.mng.exc.*;
import org.dicr.util.crypt.*;
import org.dicr.util.data.exc.*;

/**
 * Менеджер системных логинов.
 * 
 * @author Igor A Tarasov &lt;java@dicr.org&gt;
 * @version 1.0
 */
public class ShadowUtils {

	private static final Logger log = Logger.getLogger(ShadowUtils.class.getName());

	/** Default shell */
	public static final String DEFAULT_SHELL = "/bin/bash";

	/** Default no shell */
	public static final String DEFAULT_NOSHELL = "/bin/nologin";

	/** User add command */
	public static final String CMD_USERADD = "/usr/sbin/adduser";

	/** User delete command */
	public static final String CMD_USERDEL = "/usr/sbin/userdel";

	/** User modify command */
	public static final String CMD_USERMOD = "/usr/sbin/usermod";

	/** SysGroup add command */
	public static final String CMD_GROUPADD = "/usr/sbin/groupadd";

	/** SysGroup delete command */
	public static final String CMD_GROUPDEL = "/usr/sbin/groupdel";

	/** SysGroup modify command */
	public static final String CMD_GROUPMOD = "/usr/sbin/groupmod";

	/** Default Password file path */
	public static final String FILE_PASSWORD = "/etc/passwd";

	/** Password file */
	protected static File filePassword = new File(FILE_PASSWORD);

	/** User file modified time */
	protected static long filePasswordTime = 0;

	/** Default Shadow file path */
	public static final String FILE_SHADOW = "/etc/shadow";

	/** Shadow file */
	protected static File fileShadow = new File(FILE_SHADOW);

	/** Shadow file modified time */
	protected static long fileShadowTime = 0;

	/** Users cashe */
	protected static SoftReference<Map<String, SysUser>> usersRef = null;

	/** Default SysGroup file path */
	public static final String FILE_GROUP = "/etc/group";

	/** SysGroup file */
	protected static File fileGroup = new File(FILE_GROUP);

	/** SysGroup file modified time */
	protected static long groupFileTime = 0;

	/** SysGroup cache */
	protected static SoftReference<Map<String, SysGroup>> groupsRef = null;

	/**
	 * Parse password and file.
	 * 
	 * @return SusUsers map, where the key is user name
	 * @throws IOException
	 */
	protected static synchronized Map<String, SysUser> getUsersData() throws IOException {
		Map<String, SysUser> sysUsers = null;
		// try use cache first
		if (usersRef != null) sysUsers = usersRef.get();
		if (sysUsers != null && filePasswordTime != 0 && filePassword.lastModified() <= filePasswordTime
				&& fileShadow.lastModified() <= fileShadowTime) return sysUsers;
		// reset cache
		filePasswordTime = 0;
		fileShadowTime = 0;
		usersRef = null;
		// parse file
		Reader reader = null;
		try {
			// parse password file
			reader = new FileReader(filePassword);
			sysUsers = PasswdFileParser.parse(reader);
			// need close firs to get file time
			reader.close();
			reader = null;
			filePasswordTime = filePassword.lastModified();
			// parse shadow file
			reader = new FileReader(fileShadow);
			Map<String, String> passwordMap = ShadowFileParser.parse(reader);
			reader.close();
			reader = null;
			fileShadowTime = fileShadow.lastModified();
			// update users passwords
			Collection<String> userNames = sysUsers.keySet();
			for (String userName : userNames) {
				sysUsers.get(userName).passwd = passwordMap.get(userName);
			}
			// set new cache
			usersRef = new SoftReference<>(sysUsers);
		} catch (FileNotFoundException ex) {
			log.log(Level.WARNING, "file not found: " + ex.getMessage());
		} finally {
			try {
				if (reader != null) reader.close();
			} catch (Exception ex) {
				// NOP
			}
		}
		return sysUsers;
	}

	/**
	 * Read group data
	 * 
	 * @return данные групп.
	 * @throws IOException
	 *             ошибка чтения файла.
	 */
	protected static synchronized Map<String, SysGroup> getGroupsData() throws IOException {
		Map<String, SysGroup> sysGroups = null;
		// try using cache
		if (groupsRef != null) sysGroups = groupsRef.get();
		if (sysGroups != null && groupFileTime != 0 && fileGroup.lastModified() == groupFileTime) return sysGroups;
		// reset cache
		filePasswordTime = 0;
		groupsRef = null;
		Reader reader = null;
		try {
			reader = new FileReader(fileGroup);
			sysGroups = GroupFileParser.parse(reader);
			// close file before get file time
			reader.close();
			reader = null;
			groupFileTime = fileGroup.lastModified();
			groupsRef = new SoftReference<>(sysGroups);
		} finally {
			try {
				if (reader != null) reader.close();
			} catch (Exception ex) {
				// NOP
			}
		}
		return sysGroups;
	}

	/**
	 * Преобразует команду в строку.
	 * 
	 * @param command
	 *            команда.
	 * @return строка команды.
	 */
	protected static String commandToString(List<String> command) {
		assert (command != null);
		StringBuilder sb = new StringBuilder();
		for (String cp : command) {
			sb.append(cp);
			sb.append(' ');
		}
		return sb.toString();
	}

	/**
	 * Выполняет команду.
	 * 
	 * @param command
	 *            аргументы.
	 * @throws CommandException
	 *             ошибка выполнения команды.
	 */
	protected static synchronized void runCommand(List<String> command) throws CommandException {
		assert (command != null && command.size() > 0);
		log.finest("running command: " + commandToString(command));
		Process p = null;
		try {
			ProcessBuilder pb = new ProcessBuilder(command);
			pb.redirectErrorStream(true);
			p = pb.start();
		} catch (IOException ex) {
			throw new CommandException(command, ex);
		}
		List<String> out = new ArrayList<>();
		BufferedReader in = null;
		try {
			in = new BufferedReader(new InputStreamReader(p.getInputStream()));
			while (true) {
				String line = in.readLine();
				if (line == null) break;
				out.add(line);
			}
			in.close();
			in = null;
			p.waitFor();
		} catch (IOException ex) {
			throw new CommandException(command, ex);
		} catch (InterruptedException ex) {
			throw new CommandException(command, ex);
		} finally {
			try {
				if (in != null) in.close();
			} catch (IOException ex) {
				// NOP
			}
		}
		if (p.exitValue() != 0) throw new CommandException(command, out.toString());
	}

	/**
	 * Возвращает пользователей.
	 * 
	 * @return список пользователей.
	 * @throws IOException
	 *             ошибка чтения файла пользователей.
	 */
	public static synchronized Collection<SysUser> listUsers() throws IOException {
		return getUsersData().values();
	}

	/**
	 * Возвращает пользователя.
	 * 
	 * @param name
	 *            имя.
	 * @return данные пользователя.
	 * @throws NotFoundException
	 *             пользователь с данным именем не найден.
	 * @throws IOException
	 *             ошибка чтения файла.
	 */
	public static synchronized SysUser getUser(String name) throws NotFoundException, IOException {
		if (name == null) throw new IllegalArgumentException("null name");
		Map<String, SysUser> sysUsers = getUsersData();
		SysUser sysUser = sysUsers.get(name);
		if (sysUser == null) throw new NotFoundException("user '" + name + "'");
		return sysUser;
	}

	/**
	 * Создает пользователя. При создании пользователя используются только
	 * инициализированные параметры, остальные
	 * инициализируются системой по умолчанию (то же и для uid и gid).
	 * <BLOCKQUOTE><CODE>useradd -p passwd -u uid -g
	 * gid -c fullName -d homeDir -s shell</CODE></BLOCKQUOTE>
	 * 
	 * @param name
	 *            имя пользователя.
	 * @param passwd
	 *            пароль или null.
	 * @param uid
	 *            UserID или 0.
	 * @param gid
	 *            GroupID или 0.
	 * @param fullName
	 *            полное имя или null.
	 * @param homeDir
	 *            домашняя директория или null.
	 * @param shell
	 *            shell или null.
	 * @throws DuplicateException
	 *             пользователь с таким именем уже существует.
	 * @throws IOException
	 *             ошибка чтения файла.
	 * @throws CommandException
	 *             ошибка выполнения команды.
	 */
	public static synchronized void createUser(String name, String passwd, int uid, int gid, String fullName,
			String homeDir, String shell) throws DuplicateException, IOException, CommandException {
		if (name == null) throw new IllegalArgumentException("null name");
		Map<String, SysUser> sysUsers = getUsersData();
		if (sysUsers.containsKey(name)) throw new DuplicateException("user: " + name);
		List<String> command = new ArrayList<>();
		command.add(CMD_USERADD);
		if (passwd != null) {
			command.add("-p");
			command.add("'" + UnixCrypt.crypt(passwd) + "'");
		}
		if (uid != 0) {
			command.add("-u");
			command.add(Integer.toString(uid));
		}
		if (gid != 0) {
			command.add("-g");
			command.add(Integer.toString(gid));
		}
		if (fullName != null) {
			command.add("-c");
			command.add("'" + fullName + "'");
		}
		if (homeDir != null) {
			command.add("-d");
			command.add(homeDir);
		}
		if (shell != null) {
			command.add("-s");
			command.add(shell);
		}
		command.add(name);
		runCommand(command);
		log.finer("created user '" + name + "'");
	}

	/**
	 * Обновляет информацию пользователя. Обновляются только инициализированные
	 * параметры, остальные не изменяются.
	 * <BLOCKQUOTE>
	 * <CODE>usermod -u uid -g gid -c fullName -d homeDir -s shell</CODE>
	 * </BLOCKQUOTE>
	 * 
	 * @param name
	 *            имя пользователя.
	 * @param uid
	 *            UserID или 0.
	 * @param gid
	 *            GroupID или 0.
	 * @param fullName
	 *            полное имя или null.
	 * @param homeDir
	 *            домашняя директория или null.
	 * @param shell
	 *            shell или null.
	 * @throws NotFoundException
	 *             пользователь не найден.
	 * @throws IOException
	 *             ошибка чтения файла пользователей.
	 * @throws CommandException
	 *             ошибка выполнения системной команды.
	 */
	public static synchronized void updateUserInfo(String name, int uid, int gid, String fullName, String homeDir,
			String shell) throws NotFoundException, IOException, CommandException {
		if (name == null) {
			throw new IllegalArgumentException("null name");
		}
		// check user exists
		getUser(name);
		// update
		List<String> command = new ArrayList<>();
		command.add(CMD_USERMOD);
		if (uid != 0) {
			command.add("-u");
			command.add(Integer.toString(uid));
		}
		if (gid != 0) {
			command.add("-g");
			command.add(Integer.toString(gid));
		}
		if (fullName != null) {
			command.add("-c");
			command.add("'" + fullName + "'");
		}
		if (homeDir != null) {
			command.add("-d");
			command.add(homeDir);
		}
		if (shell != null) {
			command.add("-s");
			command.add(shell);
		}
		command.add(name);
		runCommand(command);
		log.finer("updated user '" + name + "'");
	}

	/**
	 * Устанавливает пароль пользователя. <BLOCKQUOTE>
	 * <CODE>usermod -p passwd</CODE></BLOCKQUOTE>
	 * 
	 * @param name
	 *            имя пользователя.
	 * @param password
	 *            новый пароль.
	 * @throws NotFoundException
	 *             пользователь не найден.
	 * @throws IOException
	 *             ошибка чтения файла пользователей.
	 * @throws CommandException
	 *             ошибка выполнения команды
	 */
	public static synchronized void setUserPasswd(String name, String password) throws NotFoundException, IOException,
			CommandException {
		if (name == null) throw new IllegalArgumentException("null user name");
		if (password == null) throw new IllegalArgumentException("null password");
		getUser(name);
		// update
		List<String> command = new ArrayList<>();
		command.add(CMD_USERMOD);
		command.add("-p");
		command.add(UnixCrypt.crypt(password));
		command.add(name);
		runCommand(command);
		log.finer("updated password of user '" + name + "'");
	}

	/**
	 * Verify user password.
	 * 
	 * @param name
	 *            user name
	 * @param password
	 *            password to verify
	 * @return true if match
	 * @throws NotFoundException
	 *             user not found
	 * @throws IOException
	 */
	public static synchronized boolean verifyUserPassword(String name, String password) throws NotFoundException,
			IOException {
		if (name == null || name.length() < 1) throw new IllegalArgumentException("empty name");
		if (password == null) throw new IllegalArgumentException("null password");
		SysUser sysUser = getUser(name);
		if (sysUser.getPasswd() == null) return false;
		String crypted = MD5Crypt.crypt(password, sysUser.getPasswd());
		return crypted.equals(sysUser.getPasswd());
	}

	/**
	 * Устанавливает shell пользователя. <BLOCKQUOTE>
	 * <CODE>usermod -s shell</CODE></BLOCKQUOTE>
	 * 
	 * @param name
	 *            имя пользователя.
	 * @param shell
	 *            shell пользователя.
	 * @throws NotFoundException
	 *             пользователь не найден.
	 * @throws IOException
	 *             ошибка чтения файла пользователей.
	 * @throws CommandException
	 *             ошибка выполнения команды
	 */
	public static synchronized void setUserShell(String name, String shell) throws NotFoundException, IOException,
			CommandException {
		if (name == null) throw new IllegalArgumentException("null user name");
		if (shell == null) throw new IllegalArgumentException("null shell");
		// check user exists
		getUser(name);
		// update
		List<String> command = new ArrayList<>();
		command.add(CMD_USERMOD);
		command.add("-s");
		command.add(shell);
		command.add(name);
		runCommand(command);
		log.finer("updated shell of user '" + name + "' to '" + shell + "'");
	}

	/**
	 * Переименовывает пользователя. <BLOCKQUOTE><CODE>usermod -l newName</CODE>
	 * </BLOCKQUOTE>
	 * 
	 * @param name
	 *            имя пользователя.
	 * @param newName
	 *            новое имя.
	 * @throws DuplicateException
	 *             новое имя уже существует.
	 * @throws NotFoundException
	 *             пользователь не найден.
	 * @throws IOException
	 *             ошибка чтения файла пользователей.
	 * @throws CommandException
	 *             ошибка выполнения команды.
	 */
	public static synchronized void renameUser(String name, String newName) throws NotFoundException,
			DuplicateException, IOException, CommandException {
		if (name == null) throw new IllegalArgumentException("null user name");
		if (newName == null) throw new IllegalArgumentException("null new user name");
		if (!SysUser.NAME_PATTERN.matcher(newName).matches()) throw new IllegalArgumentException("name: " + newName);
		Map<String, SysUser> sysUsers = getUsersData();
		if (!sysUsers.containsKey(name)) throw new NotFoundException(name);
		if (sysUsers.containsKey(newName)) throw new DuplicateException("user '" + newName + "'");
		List<String> command = new ArrayList<>();
		command.add(CMD_USERMOD);
		command.add("-l");
		command.add("'" + newName + "'");
		command.add(name);
		runCommand(command);
		log.finer("user '" + name + "' renamed to '" + newName + "'");
	}

	/**
	 * Блокирует пользователя. <BLOCKQUOTE><CODE>usermod -L</CODE></BLOCKQUOTE>
	 * 
	 * @param name
	 *            имя пользователя.
	 * @throws NotFoundException
	 *             пользователь не найден.
	 * @throws IOException
	 *             ошибка чтения файла пользователей.
	 * @throws CommandException
	 *             ошибка выполнения системной команды.
	 */
	public static synchronized void lockUser(String name) throws NotFoundException, IOException, CommandException {
		if (name == null) throw new IllegalArgumentException("null user name");
		// check exists
		getUser(name);
		// update
		List<String> command = new ArrayList<>();
		command.add(CMD_USERMOD);
		command.add("-L");
		command.add(name);
		runCommand(command);
		log.finer("locked user '" + name + "'");
	}

	/**
	 * Разблокирует пользователя. <BLOCKQUOTE><CODE>usermod -U</CODE>
	 * </BLOCKQUOTE>
	 * 
	 * @param name
	 *            имя пользователя.
	 * @throws NotFoundException
	 *             пользователь не найден.
	 * @throws IOException
	 *             ошибка чтенгия
	 * @throws CommandException
	 */
	public static synchronized void unlockUser(String name) throws NotFoundException, IOException, CommandException {
		if (name == null) throw new IllegalArgumentException("null user name");
		// check exists
		getUser(name);
		// update
		List<String> command = new ArrayList<>();
		command.add(CMD_USERMOD);
		command.add("-U");
		command.add(name);
		runCommand(command);
		log.finer("unlocked user '" + name + "'");
	}

	/**
	 * Добавляет пользователя в группу. <BLOCKQUOTE><CODE>usermod -G</CODE>
	 * </BLOCKQUOTE>
	 * 
	 * @param user
	 *            имя пользователя.
	 * @param group
	 *            имя группы.
	 * @throws NotFoundException
	 *             группа или пользователь не найдены.
	 * @throws IOException
	 *             ошибка чтения системных файлов.
	 * @throws CommandException
	 *             ошибка выполнения системной команды.
	 */
	public static synchronized void addUserToGroup(String user, String group) throws NotFoundException, IOException,
			CommandException {
		if (user == null) throw new IllegalArgumentException("null user name");
		if (group == null) throw new IllegalArgumentException("null group name");
		// check exists
		getUser(user);
		// check group exists
		getGroup(group);
		// update
		List<String> command = new ArrayList<>();
		command.add(CMD_USERMOD);
		command.add("-G");
		command.add(group);
		command.add(user);
		runCommand(command);
		log.finer("user '" + user + "' added to group '" + group + "'");
	}

	/**
	 * Удаляет пользователя. <BLOCKQUOTE><CODE>userdel</CODE></BLOCKQUOTE>
	 * 
	 * @param name
	 *            имя пользователя.
	 * @throws NotFoundException
	 *             пользователь не найден.
	 * @throws IOException
	 *             ошибка чтения файла пользователей.
	 * @throws CommandException
	 *             ошибка выполнения системной команды.
	 */
	public static synchronized void removeUser(String name) throws NotFoundException, IOException, CommandException {
		if (name == null) throw new IllegalArgumentException("null user name");
		Map<String, SysUser> sysUsers = getUsersData();
		if (!sysUsers.containsKey(name)) throw new NotFoundException(name);
		List<String> command = new ArrayList<>();
		command.add(CMD_USERDEL);
		command.add(name);
		runCommand(command);
		log.finer("removed user '" + name + "'");
	}

	/**
	 * Возвращает список групп.
	 * 
	 * @return список групп.
	 * @throws IOException
	 *             ошибка чтения файла групп.
	 */
	public static synchronized Collection<SysGroup> listGroups() throws IOException {
		return getGroupsData().values();
	}

	/**
	 * Return group
	 * 
	 * @param name
	 *            name of group
	 * @return group with given name
	 * @throws NotFoundException
	 *             group not found
	 * @throws IOException
	 */
	public static synchronized SysGroup getGroup(String name) throws NotFoundException, IOException {
		if (name == null) throw new IllegalArgumentException("null name");
		SysGroup group = getGroupsData().get(name);
		if (group == null) throw new NotFoundException("group", name);
		return group;
	}

	/**
	 * Создает группу.
	 * 
	 * @param name
	 *            имя группы.
	 * @param gid
	 *            GroupID или 0.
	 * @throws DuplicateException
	 *             группа с таким именем уже существует.
	 * @throws IOException
	 *             ошибка чтения файла групп.
	 * @throws CommandException
	 *             ошибка выполнения системной команды.
	 */
	public static synchronized void createGroup(String name, int gid) throws DuplicateException, IOException,
			CommandException {
		if (name == null) throw new IllegalArgumentException("null name");
		Map<String, SysGroup> sysGroups = getGroupsData();
		if (sysGroups.containsKey(name)) throw new DuplicateException(name);
		List<String> command = new ArrayList<>();
		command.add(CMD_GROUPADD);
		if (gid != 0) {
			command.add("-g");
			command.add(Integer.toString(gid));
		}
		command.add(name);
		runCommand(command);
		log.finer("created group '" + name + "'");
	}

	/**
	 * Обновляет GID группы.
	 * 
	 * @param group
	 *            имя группы.
	 * @param gid
	 *            SysGroup ID (>= 0).
	 * @throws NotFoundException
	 *             группы с таким именем не найдена.
	 * @throws IOException
	 *             ошибка чтения системных файлов.
	 * @throws CommandException
	 *             ошибка выполнения системной команды.
	 */
	public static synchronized void updateGroupGID(String group, int gid) throws NotFoundException, IOException,
			CommandException {
		if (group == null) throw new IllegalArgumentException("null group name");
		if (gid < 0) throw new IllegalArgumentException("gid: " + gid);
		// check exists
		getGroup(group);
		// update
		List<String> command = new ArrayList<>();
		command.add(CMD_GROUPMOD);
		command.add("-g");
		command.add(Integer.toString(gid));
		command.add(group);
		runCommand(command);
		log.finer("updated gid of group '" + group + "' to " + gid);
	}

	/**
	 * Rename group.
	 * 
	 * @param name
	 *            старое имя группы.
	 * @param newName
	 *            новое имя группы.
	 * @throws NotFoundException
	 *             группа с таким именем не найдена.
	 * @throws DuplicateException
	 *             group already exists
	 * @throws IOException
	 *             ошибка чтения системных файлов.
	 * @throws CommandException
	 *             ошибка выполнения системной команды.
	 */
	public static synchronized void renameGroup(String name, String newName) throws NotFoundException,
			DuplicateException, IOException, CommandException {
		if (name == null) throw new IllegalArgumentException("null name");
		if (newName == null) throw new IllegalArgumentException("null newName");
		if (!SysGroup.NAME_PATTERN.matcher(newName).matches()) throw new IllegalArgumentException("newName: " + newName);
		// check exists
		Map<String, SysGroup> sysGroups = getGroupsData();
		if (!sysGroups.containsKey(name)) throw new NotFoundException(name);
		if (sysGroups.containsKey(newName)) throw new DuplicateException("group", newName);
		// update
		List<String> command = new ArrayList<>();
		command.add(CMD_GROUPMOD);
		command.add("-n");
		command.add(newName);
		command.add(name);
		runCommand(command);
		log.finer("renamed group '" + name + "' to '" + newName + "'");
	}
}
